Rust基础[part9]_返回值和错误处理、模块化

Rust基础[part9]_返回值和错误处理、模块化

返回值

Option<T>

基本使用

fn option_example() {
    // 创建Option
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;

    //使用
    let x = plus_one(some_number);
    let y = plus_one(absent_number);
    println!("x: {:?}, y: {:?}", x, y);
}

fn plus_one(x: Option<i32>) -> Option<i32> {
    match x {
        None => None,
        Some(i) => Some(i + 1),
    }
}

辅助函数

  • unwrap() : 提取option中的值,但是没有值的时候会panic
fn unwrap_example() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;

    println!("some_number: {:?}", some_number.unwrap());
    println!("some_string: {:?}", some_string.unwrap());
    // println!("absent_number: {:?}", absent_number.unwrap());
}
  • is_some() 和is_none()
fn is_some_example() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;

    println!("some_number: {:?}", some_number.is_some());
    println!("some_string: {:?}", some_string.is_some());
    println!("absent_number: {:?}", absent_number.is_some());
}

fn is_none_example() {
    let some_number = Some(5);
    let some_string = Some("a string");
    let absent_number: Option<i32> = None;

    println!("some_number: {:?}", some_number.is_none());
    println!("some_string: {:?}", some_string.is_none());
    println!("absent_number: {:?}", absent_number.is_none());
}

错误处理

Rust中的错误主要分为两类:

  • 可恢复错误:通常用于系统全局角度来看可以接受的错误,例如处理用户的访问、操作等错误,这些错误只会影响某个用户自身的操作进程,而不会对系统的全局稳定性产生影响 Result<T,E>
  • 不可恢复错误:全局性或系统性的错误,会比较致命 Panic

Panic

  • panic! 是 Rust 标准库提供的宏,用于主动引发运行时错误
  • panic! 被调用时:
    • 程序打印错误信息(包括文件名、行号等)。
    • 展开(unwind)调用栈并清理资源(默认行为)。
    • 最终终止当前线程或整个程序(取决于编译配置)。

常见场景

场景 描述
panic! 显式调用 主动触发 panic
数组越界访问 vec[100] 访问长度不足的向量
unwrap()expect() 错误 OptionResultNoneErr
断言失败 使用 assert!, assert_eq! 等宏检查失败时

行为控制

1. 栈展开(Unwinding)

默认情况下,panic 会:

  • 展开调用栈(stack unwinding)
  • 执行析构函数(drop)
  • 清理资源

这是最安全的方式,但可能带来一定性能开销。

2. 中止(Aborting)

在 [Cargo.toml](file:///Users/tinachan/rust/hello_cargo/Cargo.toml) 中可以配置:

[profile.release]
panic = "abort"
  • "abort":直接终止程序,不进行栈展开(适用于嵌入式系统或性能敏感场景)
  • "unwind":默认值,保留栈展开行为

示例代码

示例 1:显式 panic
fn main() {
    panic!("这是一个主动 panic");
}
示例 2:数组越界访问
let v = vec![1, 2, 3];
println!("{}", v[99]); // 越界访问触发 panic
示例 3:使用 unwrap() 触发 panic
let s: Option<String> = None;
let _ = s.unwrap(); // 触发 panic

获取 Backtrace(调试信息)

设置环境变量以获取详细的调用栈信息:

RUST_BACKTRACE=1 cargo run

输出将包含完整的调用栈,帮助定位 panic 发生的位置。

panic 处理策略(高级)

你可以通过 std::panic::set_hook 自定义 panic 处理逻辑:

use std::panic;

panic::set_hook(Box::new(|info| {
    println!("自定义 panic 处理:{}", info);
}));

panic!("测试自定义 panic hook");

注意:此方法只能设置一次,通常用于日志记录或崩溃分析工具集成。

Result

在 Rust 中,Result 是一个标准库提供的枚举类型,用于处理可恢复错误(recoverable errors)。与 panic! 不同,Result 允许开发者通过返回值明确地处理成功或失败的情况,是编写健壮程序的核心机制之一。

定义

enum Result<T, E> {
    Ok(T),
    Err(E),
}
  • T:表示操作成功时返回的值类型。
  • E:表示操作失败时返回的错误类型。

二、常用操作

1. match 匹配

最基础也最灵活的方式:

#[test]
fn result_example() {
    match divide(19, 0) {
        Ok(result) => println!("result={}", result),
        Err(err) => println!("{}", err),
    };
}

fn divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err("除数不能为0".to_string())
    } else {
        Ok(a / b)
    }
}
2. 提取内部值: unwrap()expect(msg)
  • unwrap():如果 Ok 返回内部值;如果是 Err 则触发 panic。
  • expect(&str):类似 unwrap(),但可以自定义 panic 消息。
let result = divide(4, 0).unwrap(); // 如果出错会 panic
let result = divide(4, 0).expect("除法计算错误"); // 自定义 panic 消息

⚠️ 注意:仅在测试或确定不会出错的情况下使用。

3. 传播错误:? 运算符

用于在函数中快速传播错误。只能用于返回类型为 Result 的函数中。

fn read_file() -> Result<String, std::io::Error> {
    let content = std::fs::read_to_string("file.txt")?;
    Ok(content)
}
4. 对内部值进行修改:mapmap_errand_then
  • map(f):对 Ok 值进行映射,不影响 Err
  • map_err(f): 可以修改Err类型
  • and_then(f): 链式调用,只有当前为 Ok 时才继续执行。
let result = divide(10, 2)
    .map(|x| x * 2)
    .and_then(|x| divide(x, 0));
5. oror_else
  • or(res):如果当前是 Err,则使用给定的 Result 替代。
  • or_else(f):如果当前是 Err,则调用闭包获取替代值。
let res = divide(4, 0).or(Ok(42)); // 如果出错就返回默认值 42
6. is_ok / is_err / ok() / err()

用于判断状态或转换为 Option

if result.is_ok() {
    println!("操作成功");
}

let opt = result.ok(); // 转换为 Option<T>
7. 对返回的错误进行处理 err.kind
use std::fs::File;
use std::io::ErrorKind;
fn error_kind_example() {
    let greeting = File::open("hello.txt");

    let greeting_result = match greeting {
        Ok(result) => result,
        Err(err) => match err.kind() {
            ErrorKind::NotFound => File::create("hello.txt").unwrap(),
            other_error => panic!("Error: {}", err),
        },
    };
}

三、注意事项

  1. 避免滥用 unwrap()expect():除非你非常确定某个操作不会失败,否则应优先使用 match? 来处理错误。

  2. 错误类型保持一致:在一个项目中,建议统一使用相同的错误类型(如自定义枚举或 anyhow::Error),以便集中处理。

  3. 使用 ? 时注意函数签名:只能在返回 ResultOption 的函数中使用 ?,否则编译器会报错。

  4. 错误信息应清晰具体:不要简单返回 "error",而应该提供上下文信息,便于调试和日志分析。

  5. 配合 From trait 自动转换错误:你可以为自定义错误类型实现 From,从而简化错误传播。

impl From<std::io::Error> for MyError {
    fn from(e: std::io::Error) -> Self {
        MyError::Io(e)
    }
}

这样就可以直接使用 ?std::io::Result 转换为你的错误类型。

操作 用途
match 手动匹配成功或失败情况
unwrap() / expect() 快速获取值,出错 panic
? 向上层传递错误
map() / and_then() 链式处理成功值
or() / or_else() 提供备选错误处理方案

panic 与 Result 的区别

特性 panic! Result
类型 不可恢复错误 可恢复错误
推荐使用场景 逻辑错误、非法状态 文件读写、网络请求等
是否必须处理
对性能影响 较大(栈展开)
最佳实践
优先使用 Result 而不是 panic!
减少 unwrap() 的使用,尤其在生产代码中
统一错误类型,方便集中处理
使用 ? 简化错误传播逻辑

练习

// 修复call函数的错误
// 当b为None时,按默认值1
fn call(a: i32, b: Option<i32>) -> Result<f64, String> {
    let b = b.unwrap_or(1); // 处理b为None的情况,默认值1
    let r = match divide(a, b) {
        Some(r) => r,
        None => return Err(String::from("Division by zero")), // 捕获除零错误
    };
    let s = match sqrt(r) {
        Ok(s) => s,
        Err(MathError::NegativeSquareRoot) => {
            return Err(String::from("Cannot compute square root of a negative number")) // 捕获负数平方根错误
        }
    };
    Ok(s) // 正常返回结果
}

fn divide(a: i32, b: i32) -> Option<f64> {
    if b != 0 {
        Some(a as f64 / b as f64)
    } else {
        None
    }
}

pub enum MathError {
    DivisionByZero,
    NegativeSquareRoot,
}

fn sqrt(x: f64) -> Result<f64, MathError> {
    if x < 0.0 {
        Err(MathError::NegativeSquareRoot)
    } else {
        Ok(x.sqrt())
    }
}

模块化 Modules

Rust 的模块系统用于组织代码结构、控制作用域和可见性。它支持将程序划分为多个逻辑单元(模块),便于代码管理、复用和封装。

package

一般通过cargo new创建的就是一个package,包含library crates和 binary crates,也就是.rs

Cargo.lock
Cargo.toml
src
├── bin
│   ├── a.rs
│   └── b.rs
├── lib.rs
└── main.rs

crate

  • crate 是 Rust 中最小的编译单元,可以是一个库(library)或可执行程序(binary)。
  • 每个 crate 都有一个隐式的根模块(root module):[main.rs](file:///Users/tinachan/rust/hello_cargo/src/main.rs) 或 lib.rs
  • 所有模块都是从根模块开始组织的。

例如:

src/
├── main.rs       // 根模块
├── utils.rs      // 模块文件
└── utils/
    └── logging.rs // 子模块文件

Moudles

使用 mod 关键字定义一个模块:

mod a {
    const num: usize = 1;
    fn log1() {
        println!("{}", num);
    }
}

模块可以嵌套定义:

mod a {
    const num: usize = 1;
    fn log1() {
        println!("{}", num);
    }
    mod b {
        // 无论是func还是mod 在外部需要调用的话都需要加上pub
        const num2: usize = 2;
        fn log2() {
            println!("{}", num2);
        }
    }
}

导入模块 use

使用 use 将模块或函数引入当前作用域,简化调用:

use a::b::log2;
use a::log1;

fn main() {
    log1();
    log2(); // 内部的mod需要多层调用
}

重命名 as[重名的情况]:

 use a::{b::log as log1, log as log2};// use合并

fn main() {
    log1();
    log2(); 
}


mod a {
    const num: usize = 1;
    pub fn log() {
        println!("{}", num);
    }
    pub mod b {
        // 无论是func还是mod 在外部需要调用的话都需要加上pub
        const num2: usize = 2;
        pub fn log() {
            println!("{}", num2);
        }
    }
}

可见性(Visibility)

默认模块中的项是私有的 ; pub 关键字实现公有

fn main() {
    a::log1();
    a::b::log2(); // a内部的mod需要多层调用
}

mod a {
    const num: usize = 1;
    pub fn log1() {
        println!("{}", num);
    }
    pub mod b {
        // 无论是func还是mod 在外部需要调用的话都需要加上pub
        const num2: usize = 2;
        pub fn log2() {
            println!("{}", num2);
        }
    }
}
pub(crate)

限定在mod a中调用

pub(in crate::a) fn log() {
        println!("{}", num);
}
pub(in path)
pub use

Path

通过路径访问

  • 绝对路径
crate::b::log();// 绝对路径访问
  • 相对路径
a::log();// 相对路径访问

workspace

Rust 中用于管理 多个相关包(crate)的功能。工作区允许将多个 crate 组织在同一仓库下,共享依赖、统一构建 / 测试流程。

可以通过在cargo.toml中添加

[workspace] 
members = ["course","lib_add","lib_divide"]

image-20250716205510619